package daybook.model.transaction;

import daybook.model.*;
import daybook.model.Comparators;
import daybook.util.ImmutablePair;
import daybook.util.Pair;
import daybook.util.StreamUtils;
import daybook.util.Utils;
import daybook.util.tree.ImmutableTree;
import daybook.util.tree.Tree;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static daybook.util.StreamUtils.*;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.partitioningBy;

public final class TransactionSummary {
    private TransactionSummary() {}

    // predicate
    private static Predicate<Account> INCOME_ACCOUNT_PREDICATE = account -> account.getType() == Account.Type.REVENUE || account.getType() == Account.Type.EXPENSE;
    public static Predicate<Account> incomeAccountPredicate() {
        return INCOME_ACCOUNT_PREDICATE;
    }

    public static Predicate<Transaction> transactionPredicateByDate(LocalDate startDate, LocalDate endDate) {
        if (startDate.compareTo(endDate) >= 0) {
            throw new IllegalArgumentException("startDate >= endDate");
        }

        return tx -> !tx.getDate().isBefore(startDate) && tx.getDate().isBefore(endDate);
    }

    public static Predicate<Transaction> transactionPredicateByEntry(Predicate<TransactionEntry> transactionEntryPredicate) {
        Objects.requireNonNull(transactionEntryPredicate);

        return tx -> tx.getEntries().stream().anyMatch(transactionEntryPredicate);
    }

    public static Predicate<TransactionEntry> transactionEntryPredicateByAccount(Predicate<Account> accountPredicate) {
        Objects.requireNonNull(accountPredicate);

        return item -> accountPredicate.test(item.getAccount());
    }

    public static Predicate<TransactionEntry> transactionEntryPredicateByCommodity(Predicate<Commodity> commodityPredicate) {
        Objects.requireNonNull(commodityPredicate);

        return item -> item.getAccount().getCommodity().map(commodityPredicate::test).orElse(false);
    }

    public static Predicate<Commodity> commodityPredicate(Commodity defaultCurrency) {
        Objects.requireNonNull(defaultCurrency);
        return commodity -> !commodity.equals(defaultCurrency);
    }

    public static Predicate<Commodity> commodityPredicate(Commodity defaultCurrency, Set<Commodity> commodities) {
        return commodityPredicate(defaultCurrency).and(commodities::contains);
    }

    // filter
    private static final class DateKeyTransaction extends Transaction {
        private final LocalDate date;

        private DateKeyTransaction(LocalDate date) {
            this.date = Objects.requireNonNull(date);
        }

        @Override
        public LocalDate getDate() {
            return date;
        }

        @Override
        public Set<TransactionEntry> getEntries() {
            return Collections.emptySet();
        }
    }

    public static Stream<Transaction> filterByDate(List<Transaction> transactions, LocalDate startDate, LocalDate endDate) {
        if (transactions instanceof RandomAccess) {
            if (startDate.compareTo(endDate) >= 0) {
                throw new IllegalArgumentException("startDate >= endDate");
            }

            return Utils.binarySubList(transactions, new DateKeyTransaction(startDate), true,
                    new DateKeyTransaction(endDate), false,
                    Comparator.comparing(Transaction::getDate)).stream();
        } else {
            return transactions.stream()
                    .filter(transactionPredicateByDate(startDate, endDate));
        }
    }

    public static Stream<Transaction> filterByAccounts(Stream<Transaction> transactions, Set<Account> accounts) {
        return transactions.filter(transactionPredicateByEntry(item -> accounts.contains(item.getAccount())));
    }

    // classifier
    public static Function<TransactionEntry, List<VirtualAccount>> makeVirtualAccountClassifier(Collection<VirtualAccount> virtualAccounts) {
        if (virtualAccounts.isEmpty()) {
            return item -> Collections.emptyList();
        } else if (virtualAccounts.size() == 1) {
            return makeVirtualAccountClassifier(virtualAccounts.iterator().next());
        } else {
            return item -> virtualAccounts.stream()
                    .filter(va -> va.test(item))
                    .collect(Collectors.toList());
        }
    }

    public static Function<TransactionEntry, List<VirtualAccount>> makeVirtualAccountClassifier(VirtualAccount virtualAccount) {
        final List<VirtualAccount> vaList = Collections.singletonList(virtualAccount);
        return item -> virtualAccount.test(item) ? vaList : Collections.emptyList();
    }

    // combiner
    public static BinaryOperator<List<LedgerEntry>> ledgerEntriesCombiner() {
        return (left, right) -> {
            final BigDecimal bal = left.isEmpty() ? BigDecimal.ZERO : left.get(left.size() - 1).getBalance();
            for (LedgerEntry ledgerEntry : right) {
                left.add(ImmutableLedgerEntry.builder()
                        .from(ledgerEntry)
                        .balance(bal.add(ledgerEntry.getBalance()))
                        .build());
            }
            return left;
        };
    }

    public static BinaryOperator<List<InventoryEntry>> inventoryEntriesCombiner() {
        return  (left, right) -> {
            BigDecimal balance = BigDecimal.ZERO;
            BigDecimal countBalance = BigDecimal.ZERO;
            Map<BigDecimal, BigDecimal> costBalance = Collections.emptyMap();
            if (!left.isEmpty()) {
                final InventoryEntry lastEntry = left.get(left.size() - 1);
                balance = lastEntry.getBalance();
                countBalance = lastEntry.getCountBalance();
                costBalance = lastEntry.getCostBalance();
            }

            for (InventoryEntry inventoryEntry : right) {
                Utils.mergeInto(inventoryEntry.getCostBalance(), costBalance, BigDecimal::add);
                left.add(ImmutableInventoryEntry.builder()
                        .from(inventoryEntry)
                        .balance(balance.add(inventoryEntry.getBalance()))
                        .countBalance(countBalance.add(inventoryEntry.getCountBalance()))
                        .costBalance(inventoryEntry.getCostBalance())
                        .build());
            }
            return left;
        };
    }

    // collector
    public static <T> Collector<Transaction, ?, NavigableMap<LocalDate, T>> groupingByDate(NavigableSet<LocalDate> dates, Collector<Transaction, ?, T> collector) {
        return groupingByDate(dates, TreeMap::new, collector);
    }

    public static <T, M extends Map<LocalDate, T>> Collector<Transaction, ?, M> groupingByDate(NavigableSet<LocalDate> dates, Supplier<M> mapFactory, Collector<Transaction, ?, T> collector) {
        if (dates.size() < 1) {
            throw new IllegalArgumentException("dates.size < 1");
        }

        final Function<Transaction, LocalDate> classifier = transaction -> dates.higher(transaction.getDate());
        return Collectors.groupingBy(classifier, mapFactory, collector);
    }

    public static Collector<BigDecimal, ?, BigDecimal> amountCollector() {
        return Collectors.reducing(BigDecimal.ZERO, BigDecimal::add);
    }

    public static <T> Collector<T, ?, BigDecimal> amountCollector(Function<? super T, ? extends BigDecimal> mapper) {
        return Collectors.reducing(BigDecimal.ZERO, mapper, BigDecimal::add);
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<LedgerEntry>, List<LedgerEntry>> ledgerEntriesCollector(Supplier<List<LedgerEntry>> supplier) {
        final BiConsumer<List<LedgerEntry>, Pair<Transaction, TransactionEntry>> accumulator = (entries, pair) -> {
            final BigDecimal bal = entries.isEmpty() ? BigDecimal.ZERO: entries.get(entries.size() - 1).getBalance();
            entries.add(ImmutableLedgerEntry.of(pair.first(), pair.second(), bal.add(pair.second().getAmount())));
        };

        return Collector.of(
                supplier,
                accumulator,
                ledgerEntriesCombiner()
        );
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<LedgerEntry>, List<LedgerEntry>> ledgerEntriesCollector() {
        return ledgerEntriesCollector(ArrayList::new);
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<LedgerEntry>>, Map<T, List<LedgerEntry>>> ledgerCollector(Function<TransactionEntry, ? extends T> classifier, Function<T, BigDecimal> initBalance) {
        Objects.requireNonNull(classifier);
        Objects.requireNonNull(initBalance);
        final BiConsumer<Map<T, List<LedgerEntry>>, Pair<Transaction, TransactionEntry>> consumer = (ledger, pair) -> {
            final TransactionEntry transactionEntry = pair.second();
            final T key = Objects.requireNonNull(classifier.apply(transactionEntry), "element cannot be mapped to a null key");
            final List<LedgerEntry> entries = ledger.computeIfAbsent(key, k -> new ArrayList<>());
            final BigDecimal bal = entries.isEmpty()
                    ? Objects.requireNonNull(initBalance.apply(key))
                    : entries.get(entries.size() - 1).getBalance();
            entries.add(ImmutableLedgerEntry.of(pair.first(), transactionEntry, bal.add(transactionEntry.getAmount())));
        };
        final BinaryOperator<Map<T, List<LedgerEntry>>> combiner = (left, right) -> Utils.merge(left, right, ledgerEntriesCombiner());
        return Collector.of(
                HashMap::new,
                consumer,
                combiner
        );
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<LedgerEntry>>, Map<T, List<LedgerEntry>>> ledgerMultiCollector(Function<TransactionEntry, ? extends Iterable< ? extends T>> classifier, Function<T, BigDecimal> initBalance) {
        Objects.requireNonNull(classifier);
        Objects.requireNonNull(initBalance);
        final BiConsumer<Map<T, List<LedgerEntry>>, Pair<Transaction, TransactionEntry>> consumer = (ledger, pair) -> {
            final TransactionEntry transactionEntry = pair.second();
            for (T key: classifier.apply(transactionEntry)) {
                final List<LedgerEntry> entries = ledger.computeIfAbsent(key, k -> new ArrayList<>());
                final BigDecimal bal = entries.isEmpty()
                        ? Objects.requireNonNull(initBalance.apply(key))
                        : entries.get(entries.size() - 1).getBalance();
                entries.add(ImmutableLedgerEntry.of(pair.first(), transactionEntry, bal.add(transactionEntry.getAmount())));
            }
        };
        final BinaryOperator<Map<T, List<LedgerEntry>>> combiner = (left, right) -> Utils.merge(left, right, ledgerEntriesCombiner());
        return Collector.of(
                HashMap::new,
                consumer,
                combiner
        );
    }

    public static Collector<Item, Map<BigDecimal, BigDecimal>, Map<BigDecimal, BigDecimal>> costBalanceCollector(int dc) {
        return costBalanceCollector(dc, HashMap::new);
    }

    public static Collector<Item, Map<BigDecimal, BigDecimal>, Map<BigDecimal, BigDecimal>> costBalanceCollector(int dc, Supplier<Map<BigDecimal, BigDecimal>> supplier) {
        final BiConsumer<Map<BigDecimal, BigDecimal>, Item> accumulator = (costBalance, item) -> {
            final BigDecimal count = dc == DC.CREDIT ? item.getCount().negate() : item.getCount();
            costBalance.compute(item.getCost(), (k, v) -> v == null ? count : count.add(v));
        };
        final BinaryOperator<Map<BigDecimal, BigDecimal>> combiner = (left, right) -> Utils.merge(left, right, BigDecimal::add);
        return Collector.of(
                supplier,
                accumulator,
                combiner
        );
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<InventoryEntry>, List<InventoryEntry>> inventoryEntriesCollector() {
        return inventoryEntriesCollector(ArrayList::new);
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<InventoryEntry>, List<InventoryEntry>> inventoryEntriesCollector(Supplier<List<InventoryEntry>> supplier) {
        final BiConsumer<List<InventoryEntry>, Pair<Transaction, TransactionEntry>> accumulator = (entries, pair) -> {
            BigDecimal balance = BigDecimal.ZERO;
            BigDecimal countBalance = BigDecimal.ZERO;
            Map<BigDecimal, BigDecimal> costBalance = Collections.emptyMap();
            if (!entries.isEmpty()) {
                final InventoryEntry lastEntry = entries.get(entries.size() - 1);
                balance = lastEntry.getBalance();
                countBalance = lastEntry.getCountBalance();
                costBalance = lastEntry.getCostBalance();
            }

            entries.add(InventoryEntry.of(pair.first(), pair.second(), balance, countBalance, costBalance));
        };

        return Collector.of(
                supplier,
                accumulator,
                inventoryEntriesCombiner());
    }

    public static Collector<Pair<Transaction, TransactionEntry>, ?, InventoryEntry> inventoryEntryCollector() {
        final BiConsumer<InventoryEntry[], Pair<Transaction, TransactionEntry>> accumulator = (box, pair) -> {
            final InventoryEntry entry = box[0];
            final BigDecimal balance = entry == null ? BigDecimal.ZERO : entry.getBalance();
            final BigDecimal countBalance = entry == null ? BigDecimal.ZERO : entry.getCountBalance();
            final Map<BigDecimal, BigDecimal> costBalance = entry == null ? Collections.emptyMap() : entry.getCostBalance();

            box[0] = InventoryEntry.of(pair.first(), pair.second(), balance, countBalance, costBalance);
        };
        final BinaryOperator<InventoryEntry[]> combiner = (left, right) -> {
            if (left[0] == null) {
                return right;
            } else if (right[0] == null) {
                return left;
            } else {
                final InventoryEntry entry1 = left[0];
                final InventoryEntry entry2 = right[0];
                left[0] = ImmutableInventoryEntry.builder()
                        .transaction(entry2.getTransaction())
                        .transactionEntry(entry2.getTransactionEntry())
                        .balance(entry1.getBalance().add(entry2.getBalance()))
                        .countBalance(entry1.getCountBalance().add(entry2.getCountBalance()))
                        .costBalance(Utils.merge(entry1.getCostBalance(), entry2.getCostBalance(), BigDecimal::add))
                        .build();
                return left;
            }
        };

        return Collector.of(
                () -> new InventoryEntry[1],
                accumulator,
                combiner,
                box -> box[0]
        );
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<InventoryEntry>>, Map<T, List<InventoryEntry>>> inventoryCollector(Function<TransactionEntry, ? extends T> classifier, Function<T, BigDecimal> initBalance, Function<T, BigDecimal> initCountBalance, Function<T, Map<BigDecimal, BigDecimal>> initCostBalance) {
        final BiConsumer<Map<T, List<InventoryEntry>>, Pair<Transaction, TransactionEntry>> accumulator = (inventory, pair) -> {
            final T key = Objects.requireNonNull(classifier.apply(pair.second()), "element cannot be mapped to a null key");
            BigDecimal balance = Objects.requireNonNull(initBalance.apply(key));
            BigDecimal countBalance = Objects.requireNonNull(initCountBalance.apply(key));
            Map<BigDecimal, BigDecimal> costBalance = Objects.requireNonNull(initCostBalance.apply(key));
            final List<InventoryEntry> entries = inventory.computeIfAbsent(key, k -> new ArrayList<>());
            if (!entries.isEmpty()) {
                final InventoryEntry lastEntry = entries.get(entries.size() - 1);
                balance = lastEntry.getBalance();
                countBalance = lastEntry.getCountBalance();
                costBalance = lastEntry.getCostBalance();
            }

            entries.add(InventoryEntry.of(pair.first(), pair.second(), balance, countBalance, costBalance));
        };
        final BinaryOperator<Map<T, List<InventoryEntry>>> combiner = (left, right) -> Utils.merge(left, right, inventoryEntriesCombiner());
        return Collector.of(
                HashMap::new,
                accumulator,
                combiner
        );
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<InventoryEntry>>, Map<T, List<InventoryEntry>>> inventoryMultiCollector(Function<TransactionEntry, ? extends Iterable<? extends T>> classifier, Function<T, BigDecimal> initBalance, Function<T, BigDecimal> initCountBalance, Function<T, Map<BigDecimal, BigDecimal>> initCostBalance) {
        final BiConsumer<Map<T, List<InventoryEntry>>, Pair<Transaction, TransactionEntry>> accumulator = (inventory, pair) -> {
            for (T key: classifier.apply(pair.second())) {
                BigDecimal balance = Objects.requireNonNull(initBalance.apply(key));
                BigDecimal countBalance = Objects.requireNonNull(initCountBalance.apply(key));
                Map<BigDecimal, BigDecimal> costBalance = Objects.requireNonNull(initCostBalance.apply(key));
                final List<InventoryEntry> entries = inventory.computeIfAbsent(key, k -> new ArrayList<>());
                if (!entries.isEmpty()) {
                    final InventoryEntry lastEntry = entries.get(entries.size() - 1);
                    balance = lastEntry.getBalance();
                    countBalance = lastEntry.getCountBalance();
                    costBalance = lastEntry.getCostBalance();
                }

                entries.add(InventoryEntry.of(pair.first(), pair.second(), balance, countBalance, costBalance));
            }
        };
        final BinaryOperator<Map<T, List<InventoryEntry>>> combiner = (left, right) -> Utils.merge(left, right, inventoryEntriesCombiner());
        return Collector.of(
                HashMap::new,
                accumulator,
                combiner
        );
    }

    public static Collector<TransactionEntry, BigDecimal[], BigDecimal[]> flowCollector() {
        final Supplier<BigDecimal[]> supplier = () -> {
            final BigDecimal[] amounts = new BigDecimal[2];
            amounts[0] = amounts[1] = BigDecimal.ZERO;
            return amounts;
        };
        final BiConsumer<BigDecimal[], TransactionEntry> accumulator = (amounts, item) -> {
            final int i = (item.getAmount().signum() >= 0) ? 0 : 1;
            amounts[i] = amounts[i].add(item.getAmount());
        };
        final BinaryOperator<BigDecimal[]> combiner = (left, right) -> {
            left[0] = left[0].add(right[0]);
            left[1] = left[1].add(right[1]);
            return left;
        };

        return Collector.of(
            supplier,
            accumulator,
            combiner
        );
    }

    public static Collector<BigDecimal[], BigDecimal[], BigDecimal[]> amountsCollector() {
        final Supplier<BigDecimal[]> supplier = () -> {
            final BigDecimal[] amounts = new BigDecimal[2];
            amounts[0] = amounts[1] = BigDecimal.ZERO;
            return amounts;
        };
        final BiConsumer<BigDecimal[], BigDecimal[]> accumulator = (amounts1, amounts2) -> {
            amounts1[0] = amounts1[0].add(amounts2[0]);
            amounts1[1] = amounts1[1].add(amounts2[1]);
        };
        final BinaryOperator<BigDecimal[]> combiner = (amounts1, amounts2) -> {
            accumulator.accept(amounts1, amounts2);
            return amounts1;
        };
        return Collector.of(supplier, accumulator, combiner);
    }

    // summary
    public static Map<LocalDate, Map<VirtualAccount, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                                   NavigableSet<LocalDate> dates,
                                                                                   Set<VirtualAccount> virtualAccounts) {
        final Function<TransactionEntry, List<VirtualAccount>> classifier = makeVirtualAccountClassifier(virtualAccounts);

        return transactions.filter(transactionPredicateByDate(dates.first(), dates.last()))
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        multiGroupingBy(classifier, amountCollector(TransactionEntry::getAmount)))));
    }

    public static Map<LocalDate, Map<VirtualAccount, BigDecimal>> balance(List<Transaction> transactions,
                                                                          NavigableSet<LocalDate> dates,
                                                                          Set<VirtualAccount> virtualAccounts) {
        final Function<TransactionEntry, List<VirtualAccount>> classifier = makeVirtualAccountClassifier(virtualAccounts);

        return filterByDate(transactions, dates.first(), dates.last())
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        multiGroupingBy(classifier, amountCollector(TransactionEntry::getAmount)))));
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates) {
        return transactions.filter(transactionPredicateByDate(dates.first(), dates.last()))
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        groupingBy(TransactionEntry::getAccount, amountCollector(TransactionEntry::getAmount)))));
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(List<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates) {
        return filterByDate(transactions, dates.first(), dates.last())
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        groupingBy(TransactionEntry::getAccount, amountCollector(TransactionEntry::getAmount)))));
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates,
                                                                            Predicate<Account> accountPredicate) {
        return transactions.filter(transactionPredicateByDate(dates.first(), dates.last()))
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        filter(item -> accountPredicate.test(item.getAccount()),
                                groupingBy(TransactionEntry::getAccount, amountCollector(TransactionEntry::getAmount))))));
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(List<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates,
                                                                            Predicate<Account> accountPredicate) {
        return filterByDate(transactions, dates.first(), dates.last())
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        filter(item -> accountPredicate.test(item.getAccount()),
                                groupingBy(TransactionEntry::getAccount, amountCollector(TransactionEntry::getAmount))))));
    }

    public static void accumulateBalance(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances) {
        Map<Account, BigDecimal> lastBalanceMap = Collections.emptyMap();
        for (Map.Entry<LocalDate, Map<Account, BigDecimal>> entry: balances.entrySet()) {
            Utils.mergeInto(entry.getValue(), lastBalanceMap, BigDecimal::add);
            lastBalanceMap = entry.getValue();
        }
    }


    private static class DateKeyLedgerEntry extends LedgerEntry {
        private final LocalDate date;

        private DateKeyLedgerEntry(LocalDate date) {
            this.date = Objects.requireNonNull(date);
        }

        @Override
        public LocalDate getDate() {
            return date;
        }

        @Override
        public Transaction getTransaction() {
            return null;
        }

        @Override
        public TransactionEntry getTransactionEntry() {
            return null;
        }
    }

    private static void findBalances(List<LedgerEntry> entries, NavigableSet<LocalDate> dates, BiConsumer<LocalDate, BigDecimal> consumer) {
        if (entries.isEmpty()) {
            return;
        }

        final Comparator<LedgerEntry> c = Comparator.comparing(LedgerEntry::getDate);

        final LocalDate startDate = entries.get(0).getDate();
        final LocalDate endDate = entries.get(entries.size() - 1).getDate();
        final LocalDate to = Optional.ofNullable(dates.higher(endDate)).orElse(endDate);
        for (LocalDate date: dates.subSet(startDate, false, to, true)) {
            final int index = Utils.binaryRightIndex(entries, new DateKeyLedgerEntry(date), c, false);
            consumer.accept(date, entries.get(index).getBalance());
        }
    }

    public static <T> NavigableMap<LocalDate, Map<T, BigDecimal>> accumulatedBalance(Map<T, List<LedgerEntry>> ledger, NavigableSet<LocalDate> dates) {
        if (dates.size() < 1) {
            throw new IllegalArgumentException("dates.size < 1");
        }
        final NavigableMap<LocalDate, Map<T, BigDecimal>> map = new TreeMap<>();
        for (Map.Entry<T, List<LedgerEntry>> entry: ledger.entrySet()) {
            findBalances(entry.getValue(), dates, (date, balance) -> {
                Objects.requireNonNull(balance);
                final Map<T, BigDecimal> m = map.computeIfAbsent(date, key -> new HashMap<>());
                m.put(entry.getKey(), balance);
            });
        }

        return map;
    }

    public static void differenceBalance(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances) {
        Map.Entry<LocalDate, Map<Account, BigDecimal>> nextBalanceEntry = null;
        for (Map.Entry<LocalDate, Map<Account, BigDecimal>> entry: balances.descendingMap().entrySet()) {
            if (nextBalanceEntry != null) {
                final Map<Account, BigDecimal> nextBalanceMap = nextBalanceEntry.getValue();
                final Map<Account, BigDecimal> curBalanceMap = entry.getValue();
                for (Map.Entry<Account, BigDecimal> e: nextBalanceMap.entrySet()) {
                    final BigDecimal bal = curBalanceMap.getOrDefault(e.getKey(), BigDecimal.ZERO);
                    nextBalanceMap.put(e.getKey(), e.getValue().subtract(bal));
                }
            }
            nextBalanceEntry = entry;
        }
    }

    public static Tree<Pair<Account, BigDecimal>> summarizeBalance(Map<Account, BigDecimal> balances, Tree<Account> tree, boolean ignoreZeroBalance) {
        if (tree.isLeaf()) {
            final Account account = tree.getValue();
            final BigDecimal balance = balances.getOrDefault(account, ignoreZeroBalance ? null : BigDecimal.ZERO);
            return balance == null ? null : Tree.of(ImmutablePair.of(account, balance));
        } else {
            final List<Tree<Pair<Account, BigDecimal>>> newChildren = tree.getChildren().stream()
                    .map(t -> summarizeBalance(balances, t, ignoreZeroBalance))
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());

            if (newChildren.isEmpty()) {
                return null;
            } else {
                final Account account = tree.getValue();
                final BigDecimal balance = newChildren.stream()
                        .map(t -> t.getValue().second())
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                return ImmutableTree.of(ImmutablePair.of(account, balance), newChildren);
            }
        }
    }

    public static Tree<Pair<Account, BigDecimal>> summarizeBalance(Map<Account, BigDecimal> balances, ChartOfAccounts chartOfAccounts, boolean ignoreZeroBalance) {
        return summarizeBalance(balances, chartOfAccounts.getTree(), ignoreZeroBalance);
    }

    public static Map<VirtualAccount, List<LedgerEntry>> ledger(Stream<Transaction> transactions,
                                                                Set<VirtualAccount> virtualAccounts) {
        final Function<Pair<Transaction, TransactionEntry>, List<VirtualAccount>> classifier = makeVirtualAccountClassifier(virtualAccounts).compose(Pair::second);
        return transactions.flatMap(transaction -> transaction.getEntries().stream()
                .sorted(Comparators.itemComparator)
                .map(item -> ImmutablePair.of(transaction, item)))
                .collect(multiGroupingBy(classifier, ledgerEntriesCollector()));
    }

    public static NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> balanceTrees(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances,
                                                                                 ChartOfAccounts chartOfAccounts, boolean ignoreZeroBalances) {
        return balances.entrySet().stream()
                .collect(StreamUtils.toMap(Map.Entry::getKey,
                        e -> TransactionSummary.summarizeBalance(e.getValue(), chartOfAccounts, ignoreZeroBalances),
                        TreeMap::new));
    }

    public static Map<Account, List<LedgerEntry>> ledger(Stream<Transaction> transactions) {
        return transactions.flatMap(transaction -> transaction.getEntries().stream()
                .sorted(Comparators.itemComparator)
                .map(item -> ImmutablePair.of(transaction, item)))
                .collect(groupingBy(pair -> pair.second().getAccount(), ledgerEntriesCollector()));
    }

    public static Map<Account, List<LedgerEntry>> ledger(Stream<Transaction> transactions, Predicate<Account> predicate) {
        return transactions.flatMap(transaction -> transaction.getEntries().stream()
                .filter(transactionEntryPredicateByAccount(predicate))
                .sorted(Comparators.itemComparator)
                .map(item -> ImmutablePair.of(transaction, item)))
                .collect(groupingBy(pair -> pair.second().getAccount(), ledgerEntriesCollector()));
    }

    public static NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlow(Stream<Transaction> transactions,
                                                                                             Predicate<Account> cashAccounts,
                                                                                             NavigableSet<LocalDate> dates) {
        return transactions.filter(transactionPredicateByDate(dates.first(), dates.last())
                .and(transactionPredicateByEntry(transactionEntryPredicateByAccount(cashAccounts))))
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        partitioningBy(transactionEntryPredicateByAccount(cashAccounts),
                                groupingBy(TransactionEntry::getAccount, flowCollector())))));
    }

    public static NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlow(Stream<Transaction> transactions,
                                                                    Set<Account> cashAccounts,
                                                                    NavigableSet<LocalDate> dates) {
        return cashFlow(transactions, cashAccounts::contains, dates);
    }

    public static NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlow(List<Transaction> transactions,
                                                                                             Predicate<Account> cashAccounts,
                                                                                             NavigableSet<LocalDate> dates) {
        return filterByDate(transactions, dates.first(), dates.last())
                .filter(transactionPredicateByEntry(transactionEntryPredicateByAccount(cashAccounts)))
                .collect(groupingByDate(dates, flatMapping(tx -> tx.getEntries().stream(),
                        partitioningBy(transactionEntryPredicateByAccount(cashAccounts),
                                groupingBy(TransactionEntry::getAccount, flowCollector())))));
    }

    public static NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlow(List<Transaction> transactions,
                                                                                             Set<Account> cashAccounts,
                                                                                             NavigableSet<LocalDate> dates) {
        return cashFlow(transactions, cashAccounts::contains, dates);
    }

    public static Map<Commodity, List<InventoryEntry>> inventory(Stream<Transaction> transactions, Predicate<Commodity> commodityPredicate) {
        return transactions.flatMap(transaction -> transaction.getEntries().stream()
                .filter(transactionEntryPredicateByCommodity(commodityPredicate))
                .sorted(Comparators.itemComparator)
                .map(item -> ImmutablePair.of(transaction, item)))
                .collect(groupingBy(pair -> pair.second().getCommodity(null), inventoryEntriesCollector()));
    }

    public static List<String> tags(Stream<Transaction> transactions, Predicate<Account> accountPredicate) {
        return transactions.flatMap(tx -> tx.getEntries().stream()
                .filter(transactionEntryPredicateByAccount(accountPredicate)))
                .flatMap(item -> item.getTags().stream())
                .collect(Collectors.toList());
    }

    public static List<String> tags(Stream<Transaction> transactions) {
        return transactions.flatMap(tx -> tx.getEntries().stream())
                .flatMap(item -> item.getTags().stream())
                .collect(Collectors.toList());
    }
}
